class Bulb():
def __init__(self, onOff=False): self.onOff = onOff
def turnOn(self): self.onOff = True
def turnOff(self): self.onOff = FalsePython OOPs fundamentals
An introduction to Object Oriented programming using Python.
Increasingly it’s becoming important for Data professionals to become better at programming and modern programming is centered around Object Oriented programming paradigm. This article helps in explaining some important programming concepts which are mostly language agnostic but we will be using Python in this article.
Object-oriented programming (OOPs) is a programming paradigm that relies on the concept of classes and objects. The basic idea of OOP is to divide a sophisticated program into a number of objects that interact with each other to achieve the desired functionality. There are several advantages of using OOP for data science:
- Encapsulation: OOPs allow you to wrap data and the methods that operate on that data within a single unit (i.e., an object). This makes it easier to organize and manage your code, as well as protect the data from accidental modification.
- Code reusability: OOPs allow you to create reusable code by defining classes that can be used to create multiple objects with the same behavior. This makes it easier to develop and maintain your code, as you can reuse existing code instead of writing new code from scratch.
- Modularity: OOPs allow you to divide your code into smaller, modular units (i.e., objects). This makes it easier to understand and maintain your code, as you can focus on one piece of functionality at a time.
- Inheritance: OOPs allow you to create a new class that is a modified version of an existing class. This allows you to reuse code from the existing class and override or extend it as needed. This can save time and reduce the amount of code you need to write.
Overall, OOP can help data professionals organize and manage their code more effectively, making it easier to develop and maintain data science projects. Let’s dive into the OOPs concept.
1 What are Objects and Classes?
Classes are the blueprint for defining an Object. While an Object is a collection of data/properties and their behaviors/methods.
For example- Think of a class Bulb that will have a state (On/Off) and methods to turnOn and turnoff the bulb.
Now we can create multiple bulb objects from this Bulb class.
b1 = Bulb(onOff=True)
b2 = Bulb()
print(f"Bulb 1 state is :{b1.onOff}, Bulb 2 state is :{b2.onOff}")Bulb 1 state is :True, Bulb 2 state is :False
b1 and b2 are objects of the class Bulb. Let’s use the turnOn and turnOff methods to update the bulb properties.
b1.turnOff(); b2.turnOn()
print(f"Bulb 1 state is :{b1.onOff}, Bulb 2 state is :{b2.onOff}")Bulb 1 state is :False, Bulb 2 state is :True
We can see from the example above, a Bulb object contains the onOff property. Properties are variables that contain information regarding the object of a class and Methods like turnOn and turnOff in our Bulb class are functions that have access to the properties of a class. Methods can accept additional parameters, modify properties and return values.
2 Class and Instance variables
In Python, properties can be defined in two ways -
- Class Variables - Class variables are shared by all objects of the class. A change in the class variable will change the value of that property in all the objects of the class.
- Instance Variables - Instance variables are unique to each instance or object of the class. A change in instance variable will change the value of the property in that specific object only.
class Employee:
# Creating a class variable
companyName = "Microsoft"
def __init__(self, name):
# creating an instance variable
self.name = name
e1 = Employee('Aayush')
e2 = Employee('John')
print(f'Name :{e1.name}')
print(f'Company Name: {e1.companyName}')
print(f'Name :{e2.name}')
print(f'Company Name: {e2.companyName}')Name :Aayush
Company Name: Microsoft
Name :John
Company Name: Microsoft
We can see above, the class variable is defined outside of the initializer and the instance variable is defined inside the initializer.
Employee.companyName = "Amazon"
print(e1.companyName, e2.companyName)Amazon Amazon
We can see above changing a class variable in the Employee class changes the class variable in all objects of the class. Most of the time we will be using instance variables but knowledge about class variables can come in handy. Let’s look at an interesting use of class variable -
class Employee:
# Creating a class variable
companyName = "Microsoft"
companyEmployees = []
def __init__(self, name):
# creating an instance variable
self.name = name
self.companyEmployees.append(self.name)
e1 = Employee('Aayush')
e2 = Employee('John')
print(f'Name :{e1.name}')
print(f'Team Members: {e1.companyEmployees}')
print(f'Name :{e2.name}')
print(f'Company Name: {e2.companyEmployees}')Name :Aayush
Team Members: ['Aayush', 'John']
Name :John
Company Name: ['Aayush', 'John']
We can see above, we are saving all objects of the Employee class in companyEmployees which is a list shared by all objects of the class Employee.
3 Class, Static and Instance methods
In Python classes, we have three types of methods -
- Class Methods - Class methods work with class variables and are accessible using the class name rather than its object.
- Static Methods - Static methods are methods that are usually limited to class only and not their objects. They don’t typically modify or access class and instance variables. They are used as utility functions inside the class and we don’t want the inherited class to modify them.
- Instance Methods - Instance methods are the most used methods and have access to instance variables within the class. They can also take new parameters to perform desired operations.
class Employee:
# Creating a class variable
companyName = "Microsoft"
companyEmployees = []
def __init__(self, name):
# creating an instance variable
self.name = name
self.companyEmployees.append(self.name)
@classmethod
def getCompanyName(cls): # This is a class method
return cls.companyName
@staticmethod
def plusTwo(x): # This is a static method
return x+2
def getName(self): # This is an instance method
return self.name
e1 = Employee('Aayush')
print(f"Calling class method. Company name is {e1.getCompanyName()}")
print(f"Calling Static method. {e1.plusTwo(2)}")
print(f"Calling instance method. Employee name is {e1.getName()}")Calling class method. Company name is Microsoft
Calling Static method. 4
Calling instance method. Employee name is Aayush
We can see above we use the @classmethod decorator to define the class method. cls is used to refer to the class just as self is used to refer to the object of the class. The class method at least takes one argument cls.
We can use any other name instead of cls but cls is used as a convention.
We use @staticmethod decorator to define static class plusTwo. We can see that static methods don’t take any argument like self and cls.
The most commonly used methods are instance methods and they can be defined without a decorator within the class. Just like the class method they take at least one argument which is self by convention.
We can use any other name instead of self but self is used as a convention.
4 Access Modifiers
Access modifiers limit access to the variables and functions of a class. There are three types of access modifiers - public, protected, and private.
4.1 Public Attributes
Public attributes are those methods and properties which can be accessed anywhere inside and outside of the class. By default, all the member variables and functions are public.
class Employee:
def __init__(self, name):
self.name = name ## Public variable
def getName(self): ## Public method
return self.name
e1 = Employee("Aayush")
print(f"Employee Name: {e1.getName()}")Employee Name: Aayush
In the case above, both property name and method getName are public attributes.
4.2 Protected Attributes
Protected attributes are similar to public attributes which can be accessed within the class and also available to subclasses. The only difference is the convention, which is to define each protected member with a single underscore “_”.
class Employee:
def __init__(self, name, project):
self.name = name ## Public variable
self._project = project ## Protected variable
def getName(self): ## Public method
return self.name
def _getProject(self): ## Protected method
return self._project
e1 = Employee("Aayush", "Project Orland")
print(f"Employee Name: {e1.getName()}")
print(f"Project Name: {e1._getProject()}")Employee Name: Aayush
Project Name: Project Orland
In the case above, both property _project and method _getProject are protected attributes.
4.3 Private Attributes
Private attributes are accessible within the class but not outside of the class. To define a private attribute, prefix the method or property with the double underscore”_“.
class Employee:
def __init__(self, name, project, salary):
self.name = name ## Public variable
self._project = project ## Protected variable
self.__salary = salary
def getName(self): ## Public method
return self.name
def _getProject(self): ## Protected method
return self._project
def __getSalary(self): ## Protected method
return self.__salary
e1 = Employee("Aayush", "Project Orland", "3500")
print(f"Employee Name: {e1.getName()}")
print(f"Project Name: {e1.__getSalary()}") Employee Name: Aayush
AttributeError: 'Employee' object has no attribute '__getSalary'
We can see above, __salary property and __getSalary method are both private attributes and when we call them outside of the class they throw an error that the 'Employee' object has no attribute '__getSalary'.
5 Encapsulation
Encapsulation in OOP refers to binding data and the methods to manipulate that data together in a single unit, that is, class. Encapsulation is usually used to hide the state and representation of the object from the outside. A good use of encapsulation is to make all properties private of a class to prevent direct access from outside and use public methods to let the outside world communicate with the class.
class Employee:
def __init__(self, name, project, salary):
self.__name = name ## Public variable
self.__project = project ## Protected variable
self.__salary = salary
def getName(self): ## Public method
return self.__name
e1 = Employee("Aayush", "Project Orland", "3500")
print(f"Employee Name: {e1.getName()}")Employee Name: Aayush
Encapsulation has several advantages -
- Properties of the class can be hidden from the outside world
- More control over what the outside world can access from the class
A good example of encapsulation would be an access control class based on username and password.
class Auth:
def __init__(self, userName=None, password=None):
self.__userName = userName
self.__password = password
def login(self, userName, password):
if (self.__userName == userName) and (self.__password == password):
print (f"Access granted to {userName}")
else:
print("Invalid credentials")
e1 = Auth("Aayush", "whatever")
e1.login("Aayush", "whatever") ## This will grant access
e1.login("Aayush", "aasdasd") ## This will say invalid creds
e1.__password ## This will raise an error as private properties can't be accessed from outside.Access granted to Aayush
Invalid credentials
AttributeError: 'Auth' object has no attribute '__password'
As we can see above __username and __password are protected properties and can only be used by the class to grand or reject access requests.
6 Inheritance
Inheritance provides a way to create new classes from the existing classes. The new class will inherit all the non-private attributes(properties and methods) from the existing class. The new class can be called a child class and the existing class can be called a parent class.
import math
class Shape:
def __init__(self, name):
self.name = name
def getArea(self):
pass
def printDetails(self):
print(f"This shape is called {self.name} and area is {self.getArea()}.")
class Square(Shape):
def __init__(self, edge):
## calling the constructor from parent class Shape
Shape.__init__(self, name = "Square")
self.edge = edge
## Overiding the getArea function
def getArea(self):
return self.edge**2
class Circle(Shape):
def __init__(self, radius):
## calling the constructor from parent class Shape
Shape.__init__(self, name = "Circle")
self.radius = radius
## Overiding the getArea function
def getArea(self):
return math.pi * (self.radius**2)
obj1 = Square(4)
obj1.printDetails()
obj2 = Circle(3)
obj2.printDetails()This shape is called Square and area is 16.
This shape is called Circle and area is 28.274333882308138.
We can see above we defined a parent class Shape and then we inherited it to create a Square and Circle child class. While defining the Square and Circle class we overwrote the getArea function pertinent to the class but we used the printDetails function from the parent class to print details about child classes. The more common example in the machine learning world would be to create your own models in Pytorch where we inherit from nn.Module class to create a new model.
6.1 Use of super() Function
super() function comes into play when we implement inheritance. The super() function is used to refer to the parent class without explicitly naming the class. super() function can be used to access parent class properties, calling the parent class, and can be used as initializers. Let’s look at the example above and see how we can modify the Square class to use super() function.
class Shape:
maxArea = 100
def __init__(self, name): self.name = name
def getArea(self): pass
def printDetails(self):
print(f"This shape is called {self.name} and area is {self.getArea()}.")
class Square(Shape):
maxArea = 50
def __init__(self, edge):
super().__init__(name = "Square") ## Initializing parent class
self.edge = edge
def getName(self):
return super().maxArea
def getArea(self):
return self.edge**2
def printDetails(self):
super().printDetails() ## Calling a parent class function
print(f"Max area from Shape class: {super().maxArea}") ## Accessing parent class property
print(f"Max area from Square class: {self.maxArea}")
obj1 = Square(4)
obj1.getName()
obj1.printDetails()This shape is called Square and area is 16.
Max area from Shape class: 100
Max area from Square class: 50
As we can see in the example above we have used -
super().__init__to initialize the parentShapeclasssuper().printDetails()function to use a method from parent classsuper().maxAreato access a property of a parent class
There are many advantages of inheritance -
- Reusability - Inheritance makes the code reusable. Common methods and properties can be stored in a parent class and child classes can inherit these methods.
- Modification - Code modification becomes easier if we use inheritance, if we want to make a change in the base class function it will be propagated to the child classes.
- Extensibility - We can derive new classes from the old ones by keeping things we need in the derived class.
7 Polymorphism
Polymorphism refers to the same object exhibiting different forms and behaviors. For example consider our shape class which could be a square, rectangle, polygon, etc. Instead of writing multiple functions to get the area of these shapes, we can use a common function like getArea() and implement this function in the derived class.
import math
class Shape:
def __init__(self, name):
self.name = name
def getArea(self):
pass
def printDetails(self):
print(f"This shape is called {self.name} and area is {self.getArea()}.")
class Square(Shape):
def __init__(self, edge):
## calling the constructor from parent class Shape
Shape.__init__(self, name = "Square")
self.edge = edge
## Overiding the getArea function
def getArea(self):
return self.edge**2
class Circle(Shape):
def __init__(self, radius):
## calling the constructor from parent class Shape
Shape.__init__(self, name = "Circle")
self.radius = radius
## Overiding the getArea function
def getArea(self):
return math.pi * (self.radius**2)
obj1 = Square(4)
print(f"Area of this {obj1.name} is {obj1.getArea()}")
obj2 = Circle(3)
print(f"Area of this {obj2.name} is {obj2.getArea()}")Area of this Square is 16
Area of this Circle is 28.274333882308138
As we can see above there is a pre-defined dummy method called getArea in the Shape class. We override this method in the Square and Circle class. This technique is called method overriding. The advantage of method overriding is that the derived class can write its own specific implementation based on the requirement while using the same function name.
7.1 Abstract base classes
Abstract base classes define a set of methods and properties that a class must implement in order to inherit the parent class. This is a useful technique to enforce that certain functions within the derived class must exist. To define an abstract base class, we use the abc module. The abstract base class inherits from the built-in ABC class and we use the decorator @abstractmethod to declare an abstract method.
from abc import ABC, abstractmethod
class Shape(ABC):
def __init__(self, name):
self.name = name
@abstractmethod
def getArea(self):
pass
def printDetails(self):
print(f"This shape is called {self.name} and area is {self.getArea()}.")
class Square(Shape):
def __init__(self, edge):
## calling the constructor from parent class Shape
Shape.__init__(self, name = "Square")
self.edge = edge
obj1 = Square(4)
print(f"Area of this {obj1.name} is {obj1.getArea()}")TypeError: Can't instantiate abstract class Square with abstract methods getArea
We can see above that we have created a Shape class from the ABC class which has an abstract method getArea. Since our child class Square didn’t have getArea implemented we get an error instantiating this class.
from abc import ABC, abstractmethod
class Shape(ABC):
def __init__(self, name):
self.name = name
@abstractmethod
def getArea(self):
pass
def printDetails(self):
print(f"This shape is called {self.name} and area is {self.getArea()}.")
class Square(Shape):
def __init__(self, edge):
## calling the constructor from parent class Shape
Shape.__init__(self, name = "Square")
self.edge = edge
def getArea(self): return self.edge**2
obj1 = Square(4)
print(f"Area of this {obj1.name} is {obj1.getArea()}")Area of this Square is 16
We can see above, once we implemented the getArea method, the code runs fine.
Abstract base classes serve as a blueprint for derived classes to implement methods that are required to run the function appropriately.
8 Conclusion
In this article, we learned about what is object-oriented programming and key concepts using Python. A good understanding of these concepts will lay a solid foundation for any software professional to write and understand python code better.
I hope you enjoyed reading it. If there is any feedback on the code or just the blog post, feel free to comment below or reach out on LinkedIn.